﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.IO;
using System.Collections.Specialized;
using System.Collections;

namespace Kocic.RepositoryManagement.SystemFile
{
    /// <summary>
    /// Provides facilities for querying and working with lines in a file as objects.
    /// </summary>
    public class FileContext : IDisposable
    {
        /* The concept to the FileContext is similar to the way other LINQ providers, like EF4's one is. The FileContext reads all lines and deserializes
         * each line into a line object, storing it in a collection. Each line object has a line number and this is assigned when the line is added to the
         * collection. At this time or when line objects are deleted, line numbers are updated throughout the collection. When a line object is added to the 
         * collection, it is then considered to be "attached", whereas if it's line number is 0 it is considered detached. This context implementation doesn't
         * really implement the attach/detach mechanism like EF4 does. */

        private String _filePath = string.Empty;
        private Boolean _fileExists = false;
        private readonly ICollection<LineObject> _lineObjects = new Collection<LineObject>();
        private Boolean _isDisposed = false;

        /// <summary>
        /// Handles specific data object events.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">The event arguments.</param>
        public delegate void EventHandler(Object sender, EventArgs e);

        /// <summary>
        /// Occurs when changes are saved to the data source.
        /// </summary>
        public event EventHandler SavingChanges;

        /// Creates an instance of the FileContext class.
        public FileContext()
        {

        }

        /// <summary>
        /// Creates an instance of the FileContext class.
        /// </summary>
        /// <param name="filePath">The path to a file.</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="FileNotFoundException"></exception>
        public FileContext(String filePath)
        {
            if (String.IsNullOrEmpty(filePath))
            {
                throw new ArgumentNullException("filePath", "The file path cannot be null or empty.");
            }

            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException("The file specified by path was not found.");
            }

            _fileExists = true;

            this._filePath = filePath;

            this.LoadFileContents();
        }

        /// <summary>
        /// Gets all lines in a file.
        /// </summary>
        public IQueryable<LineObject> Lines
        {
            get
            {
                if (this._isDisposed)
                {
                    throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
                }

                return _lineObjects.AsQueryable<LineObject>();
            }
        }

        /// <summary>
        /// Gets value indicating if file exists.
        /// </summary>
        public Boolean FileExists
        {
            get
            {
                if (this._isDisposed)
                {
                    throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
                }

                return _fileExists;
            }
        }

        /// <summary>
        /// Gets the total number of lines in the file.
        /// </summary>
        public Int32 LineCount
        {
            get
            {
                if (this._isDisposed)
                {
                    throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
                }

                return this._lineObjects.Count;
            }
        }

        /// <summary>
        /// Raises the SavingChanges event.
        /// </summary>
        /// <param name="e">The event arguments.</param>
        protected virtual void OnSavingChanges(EventArgs e)
        {
            EventHandler handler = this.SavingChanges;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Adds an object to the file context.
        /// </summary>
        /// <param name="line">The line object to add to the context.</param>
        /// <exception cref="ArgumentNullException"></exception>
        public virtual void AddObject(LineObject line)
        {
            if (this._isDisposed)
            {
                throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
            }

            if (line == null)
            {
                throw new ArgumentNullException("line", "Line is null.");
            }

            checked
            {
                try
                {
                    if (String.IsNullOrEmpty(line.Contents))
                    {
                        line.BuildContents();
                    }

                    this._lineObjects.Add(line);

                    line.LNumber = this._lineObjects.Count;
                }
                catch (OverflowException ex)
                {
                    Environment.FailFast("The application has encountered a problem and must close.", ex);
                }
            }
        }

        /// <summary>
        /// Adds an object to the file context.
        /// </summary>
        /// <param name="line">The line object to add to the context.</param>
        /// <param name="lineNumber">The line number to which to insert the line object.</param>
        /// <exception cref="ArgumentNullException"></exception>
        public virtual void AddObject(LineObject line, Int32 lineNumber)
        {
            if (this._isDisposed)
            {
                throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
            }

            if (line == null)
            {
                throw new ArgumentNullException("line", "Line is null.");
            }

            if (lineNumber == 0)
            {
                throw new InvalidOperationException("Line number to which to add the line object to is invalid. The line number is not greater than 0.");
            }

            if (lineNumber > this.LineCount)
            {
                throw new InvalidOperationException("Invalid line number. The line number cannot exceed the actual line number count in the file.");
            }

            checked
            {
                try
                {
                    if (String.IsNullOrEmpty(line.Contents))
                    {
                        line.BuildContents();
                    }

                    (this._lineObjects as Collection<LineObject>).Insert(lineNumber - 1, line);

                    this.UpdateLineNumbers();
                }
                catch (ArgumentOutOfRangeException ex)
                {
                    Environment.FailFast("The application has encountered a problem and must close.", ex);
                }
                catch
                {
                    throw;
                }
            }
        }

        /// <summary>
        /// Updates the object in the file context.
        /// </summary>
        /// <param name="line">The line object to update in the context.</param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public virtual void UpdateObject(LineObject line)
        {
            if (this._isDisposed)
            {
                throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
            }

            if (line == null)
            {
                throw new ArgumentNullException("line", "Line is null.");
            }

            if (line.LineNumber == 0)
            {
                throw new InvalidOperationException("The line object cannot be updated. The line number is not greater than 0.");
            }

            var le = this._lineObjects.Where(l => l.LineNumber == line.LineNumber).SingleOrDefault();
            if (le == null)
            {
                throw new InvalidOperationException("The line object cannot be updated. No such line number exists.");
            }

            le.Contents = line.Contents;
        }

        /// <summary>
        /// Updates the object in the file context.
        /// </summary>
        /// <param name="line">The line object to update in the context.</param>
        /// <param name="lineNumber">The line number to which to update the line object at.</param>/// 
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public virtual void UpdateObject(LineObject line, Int32 lineNumber)
        {
            if (this._isDisposed)
            {
                throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
            }

            if (line == null)
            {
                throw new ArgumentNullException("line", "Line is null.");
            }

            if (line.LineNumber == 0)
            {
                throw new InvalidOperationException("The line object cannot be updated. The line number is not greater than 0.");
            }

            if (lineNumber == 0)
            {
                throw new InvalidOperationException("Line number to which to update the line object is invalid. The line number is not greater than 0.");
            }

            if (lineNumber > this.LineCount)
            {
                throw new InvalidOperationException("Invalid line number. The line number cannot exceed the actual line number count in the file.");
            }

            var le = this._lineObjects.Where(l => l.LineNumber == lineNumber - 1).SingleOrDefault();
            if (le == null)
            {
                throw new InvalidOperationException("The line object cannot be updated. No such line exists.");
            }

            le.Contents = line.Contents;
        }

        /// <summary>
        /// Deletes an object from the file context.
        /// </summary>
        /// <param name="line">The line object to delete from the context.</param>
        /// <exception cref="ArgumentNullException"></exception>
        public virtual void DeleteObject(LineObject line)
        {
            if (this._isDisposed)
            {
                throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
            }

            if (line == null)
            {
                throw new ArgumentNullException("line", "Line is null.");
            }

            if (line.LineNumber == 0)
            {
                throw new InvalidOperationException("The line object cannot be deleted. The line number is not greater than 0.");
            }

            var le = this._lineObjects.Where(l => l.LineNumber == line.LineNumber).SingleOrDefault();
            if (le == null)
            {
                throw new InvalidOperationException("The line object cannot be deleted. No such line number exists.");
            }

            this._lineObjects.Remove(le);

            this.UpdateLineNumbers();
        }

        /// <summary>
        /// Persists all changes to the file.
        /// </summary>
        public void SaveChanges()
        {
            if (this._isDisposed)
            {
                throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
            }

            this.OnSavingChanges(new EventArgs());

            lock (this)
            {
                File.WriteAllLines(_filePath, _lineObjects.Select(l => l.Contents));
            }
        }

        /// <summary>
        /// Gets the name of the file that is used as the data source.
        /// </summary>
        public String FileName
        {
            get
            {
                if (this._isDisposed)
                {
                    throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
                }

                return Path.GetFileName(this._filePath);
            }
        }

        /// <summary>
        /// Gets the path of the file used as a data source.
        /// </summary>
        public String FilePath
        {
            get
            {
                if (this._isDisposed)
                {
                    throw new ObjectDisposedException("The FileContext instance has been disposed and can no longer be used for operations.");
                }

                return this._filePath;
            }
        }

        /// <summary>
        /// Loads the lines of a file into memory.
        /// </summary>
        private void LoadFileContents()
        {
            IEnumerable<String> lines;
            Int32 lineCounter = 1;

            lock (this)
            {
                lines = File.ReadLines(_filePath);
            }

            foreach (String line in lines)
            {
                _lineObjects.Add(new LineObject()
                {
                    LNumber = lineCounter,
                    Contents = line
                });
            }
        }

        /// <summary>
        /// Updates line numbers
        /// </summary>
        private void UpdateLineNumbers()
        {
            int lineNumber = 1;
            foreach (LineObject l in this._lineObjects)
            {
                l.LNumber = ++lineNumber;
            }
        }

        /// <summary>
        /// Disposes unmanaged resources used by the context.
        /// </summary>
        /// <param name="disposing">Value indicating if to invoke the method from the IDisposable.Dispose implementation or from the finalizer.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this._isDisposed)
                {
                    return;
                }

                _lineObjects.Clear();

                this._isDisposed = true;
            }
        }

        #region IDisposable Members

        /// <summary>
        /// Disposes resources used by the manager.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion
    }
}
